#!/usr/bin/env python4
# -*- coding: utf-8 -*-
"""
第4周作业：

通过Python编程完成一个银行ATM机模拟系统，具备如下功能

(1)登陆验证：用户输入用户名密码登陆，检测用户名是否存在以及用户名密码是否匹配；用户名密码共有三次输入机会，超过三次系统退出。

(2)菜单界面：登陆成功后显示功能操作界面，输入序号选择对应功能。

(3)用户注册：用户可以输入用户名和密码创建自己的账号，并输入电话号码等信息，如果用户名存在则让用户重新输入用户名。注册后免费赠送5000元余额。

(4)账户管理：用户可以随时查看自己的账户余额。用户可以输入其他账户用户名，实现转账功能；转账时用户名必须存在，否则提示异常。用户也可以模拟实现存取款功能。

(5)用户名和密码以及账户信息等必须永久保存。且基于命令行完成，不需要开发GUI界面。
"""

import getpass
import hmac
import json
import sys


class User:
    def __init__(self, name: str, password: str, balance: float = 0.0):
        self.name = name
        self.pw = password
        self.balance = balance
        self.encrypt = self.encrypt_password()

    def encrypt_password(self) -> str:
        bytes_name = self.name.encode('utf-8')
        bytes_pw = self.pw.encode('utf-8')

        return hmac.new(bytes_name, bytes_pw, digestmod='MD5').hexdigest()


class ATM:
    PRE_MENU = ('sign_up', 'login')
    LOGIN_MENU = ('check balance', 'transfer', 'withdraw', 'deposit', 'logout')

    def __init__(self, db_name: str) -> None:
        self.db_file = db_name
        self.login_user = None
        self.users = self.readDB()
        self.login_success = False
        self.pre_opt = {
            1: self.register,
            2: self.login,
        }
        self.login_opt = {
            1: self.display_balance,
            2: self.transfer,
            3: self.withdraw,
            4: self.deposit,
            5: self.logout,
        }

    def readDB(self) -> dict:
        users = {}
        try:
            users = self._readDB()
        except FileNotFoundError:
            # 初次创建 初始化users
            self.users = {}
            msg = 'No account in DB.\nPlease register first.'
            ATM._tips(msg)
            self.register(first_time=True)
            users = self._readDB()
        finally:
            return users

    def _readDB(self) -> dict:
        with open(self.db_file, 'r') as f:
            users = json.load(f)
        return users

    def run(self):
        while True:
            opt = self._select_menu(ATM.PRE_MENU)
            self.pre_opt.get(opt, self._tips)()
            while self.login_success:
                login_opt = self._select_menu(ATM.LOGIN_MENU)
                self.login_opt.get(login_opt, self._tips)()

    def register(self, first_time: bool = False) -> None:
        while True:
            try:
                name = input('User name >> ')
                # 注册名必须为字母, 注册名不能已存在
                if not name.strip().isalpha() or self.duplicate(
                        name, first_time):
                    msg1 = 'User name should be letter only.'
                    msg2 = 'User name need to be unique !'
                    ATM._tips(msg1 + '\n' + msg2)
                    continue

                pw = getpass.getpass('Password >> ')
                if pw != getpass.getpass('Password again >> '):
                    msg = 'Your second typing not match the first time.'
                    ATM._tips(msg + '\n---')
                    continue
                else:
                    self._register(name, pw)
                    break
            except KeyboardInterrupt:
                print('')
                break

    def _register(self, name: str, pw: str) -> None:
        sign_up_gift = 5000.00
        u = User(name, pw, sign_up_gift)
        self.users[name] = dict(username=u.name,
                                encrypt=u.encrypt,
                                balance=u.balance)
        self._updateDB()

    def duplicate(self, name, first_time):
        if first_time:
            return False
        return bool(self.users.get(name, None))

    def login(self) -> None:
        times = 0
        while times < 3:
            name = input('Login: ')
            password = getpass.getpass('Password: ')
            user_item = self.users.get(name, None)
            if user_item:
                u = User(name, password)
                if u.encrypt == user_item['encrypt']:
                    # 记录登录成功的用户
                    self.login_user = user_item['username']
                    msg = 'Login success.\nWelcome back `{}`:'.format(u.name)
                    self._tips(msg)
                    self.login_success = True
                    break
            else:
                msg = (
                    'Username not exist or password incorrect.\n'
                    'Please try again.'
                )
                self._tips(msg)
            times += 1
        if not self.login_success:
            msg = 'Over 3 times incorrect, exit!'
            self._tips(msg)

    def display_balance(self) -> None:
        balance_fmt = 'Your balance: {}'
        print(balance_fmt.format(self._get_balance(self.login_user)))

    def transfer(self) -> None:
        try:
            to_user = input('Transfer username: ')
        except KeyboardInterrupt:
            print('')
            pass
        else:
            if self.users.get(to_user, None) and to_user != self.login_user:
                banner = 'How much money you want to trasfer?'
                amount = self._asking_amount(banner=banner)
                if amount:
                    self._set_balance(self.login_user, -amount)
                    self._set_balance(to_user, amount)
                    self._updateDB()
            else:
                msg = 'Username not exist, exit.'
                self._tips(msg)

    def withdraw(self) -> None:
        banner = 'How much money you want to withdraw ?'
        amount = self._asking_amount(banner=banner)
        self.users[self.login_user]['balance'] -= amount
        self._updateDB()

    def deposit(self):
        banner = 'How much money you want to deposit ?'
        amount = self._asking_amount(banner=banner, need_cmp=False)
        self._set_balance(self.login_user, amount)
        self._updateDB()

    def logout(self):
        self.login_success = False

    def _get_balance(self, user: str) -> float:
        return self.users[user]['balance']

    def _set_balance(self, user: str, amount: float) -> None:
        self.users[user]['balance'] += amount

    def _asking_amount(self,
                       over: int = 3,
                       times: int = 0,
                       banner: str = '',
                       need_cmp: bool = True) -> float:
        amount = 0.0
        while times < over:
            try:
                amount = input(banner + '\n(Ctrl+C exit)>> ')
            except KeyboardInterrupt:
                break
            else:
                if not amount.strip().isdigit():
                    msg = 'Amount should be digit!'
                    self._tips(msg)
                else:
                    amount = float(amount)
                    cal_balance = self._get_balance(self.login_user) - amount
                    if cal_balance < 0 and need_cmp:
                        msg = "Insufficient balance!"
                        self._tips(msg)
                    else:
                        break
            times += 1
        if times == over:
            amount = 0.0
            msg = 'After three times error, try again later...'
            self._tips(msg)

        return amount

    def _updateDB(self):
        with open(self.db_file, 'w') as db:
            json.dump(self.users, db, indent=4, sort_keys=True)

    @staticmethod
    def _select_menu(menu: tuple) -> int:
        # return `None` if error input
        for index, opt in enumerate(menu, 1):
            print('[{}] {}'.format(index, opt))

        raw_select = input('>> ')

        select = raw_select.strip()
        if select.isdigit():
            return int(select)

    @staticmethod
    def _tips(msg='Error input, try again.', is_error: bool = False):
        print(msg)
        if is_error:
            sys.exit(-1)


def main():
    db_name = 'users.json'
    atm = ATM(db_name)
    atm.run()


if __name__ == '__main__':
    main()
    # 代码写的很标准